/*
 * File:    HttpClientContext.java
 * Created: 18-Dec-2006
 * Version: $Id$
 *
 * COPYRIGHT (C) 2006, Bitgate Software, LLC.  All Rights Reserved.
 * RELEASED UNDER THE CREATIVE COMMONS LICENSE.
 *
 * kenji@bitgatesoftware.com
 */

package com.bitgate.util.service.protocol.http;

import static com.bitgate.util.debug.Debug.debug;
import static com.bitgate.util.debug.Debug.info;
import static com.bitgate.util.debug.Debug.isDebugEnabled;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.channels.ClosedChannelException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import com.bitgate.util.buffer.BufferException;
import com.bitgate.util.buffer.BufferFactory;
import com.bitgate.util.buffer.BufferInterface;
import com.bitgate.util.bytes.ByteString;
import com.bitgate.util.service.client.ClientContext;
import com.bitgate.util.service.protocol.http.Http.HttpRequestObject;
import com.bitgate.util.service.protocol.http.handler.HttpHeaders;
import com.bitgate.util.service.protocol.http.store.CookieStore;
import com.bitgate.util.socket.ClientChannel;

/**
 * This class is used to contain information about a currently connected client, and the client context information.
 * It provides a way to get at the client data, which is stored in the <code>ClientContext</code>, and later used for
 * parsing for requests.  This is a supporting class that is used with the <code>Http</code> protocol service.
 *
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @version $Id$
 */
public class HttpClientContext
{
    private final ClientContext cContext;
    private final Map<ByteString, ByteString> headers;
    private final Map<String, String> properties;
    private final Map<String, CookieStore> incomingCookies;
    private final Map<String, CookieStore> outgoingCookies;
    private final AtomicInteger numCompletedRequests = new AtomicInteger(0);
    
    private String originalRequest;
    private String mimeType;
    private HttpRequestObject httpRequestObject;
    private HttpVariableStore httpVariableStore;
    private HttpHeaders.HeaderResult httpResult;
    private byte[] dataStorage;
    private BufferInterface filePostStorage;
    private ParseMode currentParseMode;
    
    /** Parsing mode currently set in the HTTP context. */
    public enum ParseMode {
    	/** Parsing headers. */
    	HEADER,
    	
    	/** Parsing body data. */
    	BODY
    }

    /**
     * Constructor - stores the <code>ClientContext</code> object of the currently connected client.
     *
     * @param cContext The <code>ClientContext</code> object.
     */
    public HttpClientContext(ClientContext cContext)
    {
		this.cContext = cContext;
		this.mimeType = null;
		this.httpRequestObject = null;
		this.httpVariableStore = null;
		this.dataStorage = null;
		this.filePostStorage = null;
		this.currentParseMode = ParseMode.HEADER;
		this.headers = new HashMap<ByteString, ByteString>();
		this.properties = new HashMap<String, String>();
		this.incomingCookies = new HashMap<String, CookieStore>();
		this.outgoingCookies = new HashMap<String, CookieStore>();
		this.originalRequest = null;
		this.httpResult = null;
		
		if (isDebugEnabled()) {
			debug("Added client context.");
		}
    }

    /**
     * Returns the currently stored <code>ClientContext</code> object.
     *
     * @return <code>ClientContext</code> object.
     */
    public ClientContext getClientContext()
    {
    	return this.cContext;
    }
    
    /**
     * Sets the variable storage object.
     * 
     * @param httpVariableStore <code>HttpVariableStore</code> object to set.
     */
    public void setVariableStore(HttpVariableStore httpVariableStore)
    {
    	this.httpVariableStore = httpVariableStore;
    }
    
    /**
     * Returns the variable store object.
     * 
     * @return <code>HttpVariableStore</code> object.
     */
    public HttpVariableStore getVariableStore()
    {
    	return this.httpVariableStore;
    }
    
    /**
     * Sets the HTTP Request object.
     * 
     * @param httpRequestObject <code>HttpRequestObject</code> object.
     */
    public void setHttpRequestObject(HttpRequestObject httpRequestObject)
    {
    	this.httpRequestObject = httpRequestObject;
    	
    	if (isDebugEnabled()) {
    		debug("HTTP request object set to '" + httpRequestObject + "'");
    	}
    }
    
    /**
     * Returns the HTTP Request object.
     * 
     * @return <code>HttpRequestObject</code> object.
     */
    public HttpRequestObject getHttpRequestObject()
    {
    	return this.httpRequestObject;
    }
    
    /**
     * Sets the data to send to the client.
     * 
     * @param data <code>byte[]</code> containing the data to send.
     */
    public void setData(byte[] data)
    {
    	if (dataStorage != null) {
    		byte[] tempData = new byte[dataStorage.length + data.length];
    		
    		System.arraycopy(dataStorage, 0, tempData, 0, dataStorage.length);
    		System.arraycopy(data, 0, tempData, dataStorage.length, data.length);
    		
    		dataStorage = tempData;
    		
    		// Garbage collect.
    		tempData = null;
    	} else {
    		dataStorage = data;
    	}
    }
    
    /**
     * Returns the data to send to the client.
     * 
     * @return <code>byte[]</code> containing the data to return.
     */
    public byte[] getData()
    {
    	return this.dataStorage;
    }
    
    /**
     * Clears out the data storage object.
     */
    public void clearData()
    {
    	this.dataStorage = null;
    }
    
    /**
     * Adds a chunk of data to be sent via the buffer.
     * 
     * @param data <code>byte[]</code> data to send.
     */
    public void addBufferChunk(byte[] data)
    {
    	if (data == null) {
    		return;
    	}
    	
    	if (filePostStorage == null) {
    		try {
    			filePostStorage = BufferFactory.getInstance().get(BufferFactory.BUFFER_MEMORY);
    		} catch(BufferException e) {
    			if (isDebugEnabled()) {
    				debug("Unable to create a new memory buffer object: " + e.getMessage());
    			}
    			
    			return;
    		}
    	}
    	
    	try {
    		filePostStorage.appendData(data);
    	} catch(BufferException e) {
    		if (isDebugEnabled()) {
    			debug("Unable to append data to memory buffer: " + e.getMessage());
    		}
    	}
    	
    	if (isDebugEnabled()) {
    		debug("Added data length '" + data.length + "' to storage buffer.");
    	}
    }
    
    /**
     * Retrieves the next buffer chunk of data.
     * 
     * @return <code>byte[]</code> object.
     * @throws NullPointerException on any errors.
     */
    public byte[] getBufferChunk() throws NullPointerException
    {
    	return filePostStorage.take();
    }
    
    /**
     * Returns the size of the buffer in use for the POST data.
     * 
     * @return <code>long</code> containing the post data size.
     */
    public long getBufferSize() {
    	return (filePostStorage != null) ? filePostStorage.size() : 0L;
    }
    
    /**
     * Returns the currently active parsing mode.
     * 
     * @return <code>ParseMode</code> object.
     */
    public ParseMode getParseMode()
    {
    	return currentParseMode;
    }
    
    /**
     * Sets the currently active parsing mode.
     * 
     * @param parseMode <code>ParseMode</code> to set.
     */
    public void setParseMode(ParseMode parseMode)
    {
    	currentParseMode = parseMode;
    }
    
    /**
     * Sets a header object with the specified value.
     * 
     * @param header <code>ByteString</code> containing the header object.
     * @param value <code>ByteString</code> containing the value.
     */
    public void setHeader(ByteString header, ByteString value)
    {
    	if (headers.get(header) != null) {
    		if (isDebugEnabled()) {
    			debug("Header = '" + header + "' Value += '" + value + "'");
    		}
    		
    		headers.get(header).append(value.getBytes());
    		return;
    	}
    	
    	if (isDebugEnabled()) {
    		debug("Header = '" + header + "' Value = '" + value + "'");
    	}
    	
    	headers.put(header, value);
    }
    
    /**
     * Returns the header value.
     * 
     * @param header <code>ByteString</code> to look up.
     * @return <code>ByteString</code> returning the header value.
     */
    public ByteString getHeader(ByteString header)
    {
    	return headers.get(header);
    }
    
    /**
     * Sets the original request that was made to the server (GET/POST/HEAD/etc.)
     * 
     * @param originalRequest <code>String</code> containing the original request.
     */
    public void setOriginalRequest(String originalRequest)
    {
    	this.originalRequest = originalRequest;
    }
    
    /**
     * Returns the original request.
     * 
     * @return <code>String</code> containing the original request.
     */
    public String getOriginalRequest()
    {
    	return this.originalRequest;
    }
    
    /**
     * Returns the number of requests made.
     * 
     * @return <code>int</code> containing the number of requests.
     */
    public int getNumCompletedRequests()
    {
    	return this.numCompletedRequests.get();
    }
    
    /**
     * Sets the mime type.
     * 
     * @param mimeType <code>String</code> containing the mime type.
     */
    public void setMimeType(String mimeType)
    {
    	this.mimeType = mimeType;
    }
    
    /**
     * Returns the mime type.
     * 
     * @return <code>String</code> containing the mime type.
     */
    public String getMimeType()
    {
    	return this.mimeType;
    }
    
    /**
     * Sets a property to the client context object.
     * 
     * @param key The <code>String</code> key name to use.
     * @param value The <code>String</code> containing the data to set.
     */
    public void setProperty(String key, String value) {
    	properties.put(key.toLowerCase(), value);
    }
    
    /**
     * Returns a property stored in the properties database.
     * 
     * @param key A <code>String</code> containing the key to look up.
     * @return <code>String</code> containing the value, <code>null</code> if not found or not set.
     */
    public String getProperty(String key) {
    	return properties.get(key.toLowerCase());
    }
    
    /**
     * Sets the HTTP result code for the current request.
     * 
     * @param httpResult <code>HttpHeaders.HeaderResult</code> object.
     */
    public void setRequestResult(HttpHeaders.HeaderResult httpResult) {
    	this.httpResult = httpResult;
    }
    
    /**
     * Returns the header result code that was generated when the request was made to the server.
     * 
     * @return <code>HttpHeaders.HeaderResult</code> on success, <code>null</code> if no code was sampled.
     */
    public HttpHeaders.HeaderResult getRequestResult() {
    	return this.httpResult;
    }
    
    /**
     * This is a shortcut to write data to a client connection, instead of having to use the {@link ClientContext} object.
     * 
     * @param data The <code>byte[]</code> data to write out.
     * @return {@link ClientChannel} connected to this client.
     * @throws ClosedChannelException on any errors.
     */
    public ClientChannel write(byte[] data) throws ClosedChannelException {
    	return this.cContext.getClientChannel().write(data);
    }
    
    /**
     * Writes data to the client using a file pointer.  This is not very efficient; this should be rewritten to use write
     * from the {@link java.io.File} object properly.
     * 
     * @param file A {@link java.io.File} object to read from.
     * @return {@link ClientChannel} object connected to this client.
     * @throws ClosedChannelException on any errors.
     */
    public ClientChannel write(File file) throws ClosedChannelException {
    	InputStream is = null;
    	
    	try {
    		is = new FileInputStream(file);
    	} catch(Exception e) {
    		info("Client request for file '" + file.getName() + "' failed: " + e.getMessage());
    		return null;
    	}
    	
    	long length = file.length();
    	byte[] data = new byte[(int) length];
    	int offset = 0;
    	int numRead = 0;
    	
    	try {
	    	while(offset < data.length && (numRead = is.read(data, offset, data.length - offset)) >= 0) {
	            offset += numRead;
	        }
    	} catch(Exception e) {
    		info("Unable to read data: " + e.getMessage());
    		return null;
    	}
    	
    	try {
    		is.close();
    	} catch(Exception e) {
    		// Do nothing.
    	}
    	
    	return write(data);
    }
    
    /**
     * Stores a cookie that was sent by the requesting client.
     * 
     * @param name The name of the cookie.
     * @param cookie The <code>CookieStore</code> object to store.
     */
    public void setIncomingCookie(String name, CookieStore cookie) {
    	incomingCookies.put(name, cookie);
    }
    
    /**
     * Sets a cookie to be sent back to the client.
     * 
     * @param name The name of the cookie.
     * @param cookie The <code>CookieStore</code> object to store.
     */
    public void setOutgoingCookie(String name, CookieStore cookie) {
    	outgoingCookies.put(name, cookie);
    }
    
    /**
     * Returns a cookie that was sent by the client.
     * 
     * @param name The name of the cookie to retrieve.
     * @return <code>CookieStore</code> object if found, <code>null</code> otherwise.
     */
    public CookieStore getIncomingCookie(String name) {
    	return incomingCookies.get(name);
    }
    
    /**
     * Returns a cookie that will be sent to the client.
     * 
     * @param name The name of the cookie to retrieve.
     * @return <code>CookieStore</code> object if found, <code>null</code> otherwise.
     */
    public CookieStore getOutgoingCookie(String name) {
    	return outgoingCookies.get(name);
    }
    
    /**
     * Returns a set of strings that have been reserved for outgoing cookies.
     * 
     * @return <code>Set&lt;String&gt;</code> used for identifying the outgoing cookies to set to the client.
     */
    public Set<String> getOutgoingCookieSet() {
    	return outgoingCookies.keySet();
    }
    
    /**
     * This function is used to "reset" the state of the HTTP Client Context - this is used for keep-alive connections.
     */
    public void reset()
    {
		this.dataStorage = null;
		this.mimeType = null;
		this.filePostStorage = null;
		this.currentParseMode = ParseMode.HEADER;
		this.headers.clear();
		this.incomingCookies.clear();
		this.outgoingCookies.clear();
		this.originalRequest = null;
		this.numCompletedRequests.incrementAndGet();
		this.httpRequestObject = null;
		this.httpVariableStore = null;
		this.currentParseMode = ParseMode.HEADER;
		this.httpResult = null;
		
		if (isDebugEnabled()) {
			debug("Added client context.");
		}
    }
}